package com.lipaluma.csv.parser;

import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.Reader;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;

import com.lipaluma.context.Context;
import com.lipaluma.context.mapping.impl.ColumnMappingManager;
import com.lipaluma.csv.annotations.config.CSV;
import com.lipaluma.csv.config.CsvConfiguration;
import com.lipaluma.csv.xml.XmlCsvMappings;
import com.lipaluma.csv.xml.configuration.XmlCsvConfiguration;
import com.lipaluma.exceptions.CsvParserException;
import com.lipaluma.exceptions.CsvParserProcessException;
import com.lipaluma.field.mapper.FieldMapper;
import com.lipaluma.field.mapper.mapping.Column;
import com.lipaluma.field.mapper.mapping.MappingInfo;
import com.lipaluma.io.Row;
import com.lipaluma.io.SourceReader;
import com.lipaluma.parser.AbstractDataParser;
import com.lipaluma.parser.AbstractDataParserIterable;
import com.lipaluma.util.FileUtils;
import com.lipaluma.xml.XmlMappings;
import com.lipaluma.xml.configuration.XmlConfiguration;

public class CsvParserIterable<TARGET> extends AbstractDataParserIterable<TARGET, CsvConfiguration> {
	/**
	 * Constructor with the target class defined where the mapping will be defined by annotations
	 * @param clazz target class
	 */
	private CsvParserIterable(Class<TARGET> clazz) {
		super(clazz);
	}
	/**
	 * Constructor with the target class defined where the mapping will be defined by xml
	 * @param clazz target class
	 * @param xml mapping xml
	 */
	private CsvParserIterable(Class<TARGET> clazz, XmlMappings xmlMappings) {
		super(clazz, xmlMappings);
	}

	private CsvParserIterable(Builder<TARGET> builder) {
		super(builder);
	}
	
	@Override
	protected Context createContext() {
		return new Context(new ColumnMappingManager());
	}
	
	/**
	 * Construct a RowMapper for the defined target entity class with annotation declaration
	 * 
	 * @param clazz the target entity class
	 * @return the RowMapper
	 */
	public static <TARGET> CsvParserIterable<TARGET> forClass(Class<TARGET> clazz) {
		return new CsvParserIterable<TARGET>(clazz); 
	}

	public static <TARGET> CsvParserIterable<TARGET> byXml(Class<TARGET> clazz, InputStream xmlInput) {
		try {
			JAXBContext context = JAXBContext.newInstance(XmlCsvMappings.class);
			Unmarshaller unmarshaller = context.createUnmarshaller();
			XmlMappings xmlMappings = (XmlMappings)unmarshaller.unmarshal(xmlInput);
			return new CsvParserIterable<TARGET>(clazz, xmlMappings);
		} catch (JAXBException e) {
			throw new CsvParserException("Error on parsing the xml file of mappings", e);
		}
	}

	/**
	 * Construct a Mapper for the entity target class
	 * 
	 * @param file path of the xml file configuration
	 * @param clazz target class
	 * @return row mapper
	 */
	public static <TARGET> CsvParserIterable<TARGET> byXml(Class<TARGET> clazz, String xmlFile) throws FileNotFoundException {
		return byXml(clazz, FileUtils.getInputStreamFromFilePath(xmlFile));
	}

	@Override
	protected CsvConfiguration createConfiguration(Class<TARGET> clazz) {
		if(clazz.isAnnotationPresent(CSV.class))
			return new CsvConfiguration(clazz.getAnnotation(CSV.class));
		else
			return new CsvConfiguration();
	}

	@Override
	protected CsvConfiguration createConfiguration(Class<TARGET> clazz, XmlConfiguration configuration) {
		XmlCsvConfiguration csvConfiguration = (XmlCsvConfiguration)configuration;
		return new CsvConfiguration(csvConfiguration);
	}

	private void processHeader(SourceReader fileReader) {
		if(configuration.hasHeader()) {
			for(int i=1; i<=configuration.getLinesHeaderNumber(); i++) {
				Row row = fileReader.next();
				if(configuration.getHeaderLine() == i && configuration.isColumnsMappedByHeaders()){
					mapHeaders(row.toArray());
				}
			}
		} else if(configuration.isColumnsMappedByHeaders())
			throw new CsvParserProcessException("Error in CSV configuration : mapping by header's name is mentioned without header");
	}
	
	public void mapHeaders(String[] headers) {
		for(FieldMapper mapper : fieldMappers) {
			for (MappingInfo mapping : mapper.getMappings()) {
				if(Column.class.isAssignableFrom(mapping.getClass())){
					for (int index = 0; index < headers.length; index++) {
						if(((Column)mapping).mapHeader(headers[index], index))
							break;
					}
				}
			}
		}
	}
	
	/**
	 * define the input file entry (CSV or excel) by the file path
	 * <p>
	 * The type of the file is determined by the extension of the filename to recover the FileType defined in the POJO.<br/>
	 * 
	 * If the file is in the classpath, put the keyword 'classpath:' before the filename path.
	 * 
	 * @param filepath the path to the file. To search in the classpath, put 'classpath:' before.
	 * @param header true if entry file has header
	 * @throws FileNotFoundException when the file is not found
	 */
	public void withFile(String filepath) throws FileNotFoundException {
		InputStream inputStream = FileUtils.getInputStreamFromFilePath(filepath);
		SourceReader sourceReader = sourceFactory.createSourceReader(inputStream);
		setSourceReader(sourceReader);
		processHeader(sourceReader);
	}

	/**
	 * define the input file entry (CSV or excel) by a reader
	 * <p>
	 * The type of the file is defined by the configuration defined on the POJO. <br/>
	 * If several types are defined, the order of the default configuration is
	 * CSV, Excel, XLSX.
	 * 
	 * @param reader the csv or excel file entry 
	 * @param header if true, the process will skip one line and will try to map column names
	 */
	public void withReader(Reader reader) {
		SourceReader sourceReader = sourceFactory.createSourceReader(reader);
		setSourceReader(sourceReader);
		processHeader(sourceReader);
	}

	/**
	 * define the input file entry (CSV or excel) by a inputStream
	 * <p>
	 * The type of the file is defined by the configuration defined on the POJO. <br/>
	 * If several types are defined, the order of the default configuration is
	 * CSV, Excel, XLSX.

	 * @param in the csv or excel file entry 
	 * @param header if true, the process will skip one line and will try to map column names
	 */
	public void withInputStream(InputStream in)  {
		SourceReader sourceReader = sourceFactory.createSourceReader(in);
		setSourceReader(sourceReader);
		processHeader(sourceReader);
	}

	public static <TARGET> Builder<TARGET> builder(Class<TARGET> clazz) {
		return new Builder<TARGET>(clazz);
	}
	
	public static class Builder<TARGET> extends AbstractDataParser.Builder<CsvParserIterable<TARGET>, TARGET, CsvConfiguration> {
		private CsvConfiguration configuration;
		
		public Builder(Class<TARGET> clazz) {
			super(clazz, new Context(new ColumnMappingManager()));
		}
				
		public CsvParserIterable<TARGET> build() {
			return new CsvParserIterable<TARGET>(this);
		}

	}
}
